%% @author Carlos Abalde <carlos.abalde@gmail.com>
%% @doc    ...

%%%----------------------------------------------------------------------
%%%
%%% EMC, Copyright (C) 2009 Carlos Abalde
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License
%%% along with this program; if not, write to the Free Software
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
%%% 02111-1307 USA
%%%
%%%----------------------------------------------------------------------

-module(emc.cpool).
-behaviour(gen_server).

-import(lists).
-import(io_lib).
-import(gen_server).
-import(erlang).
-import(emc.util.config).
-import(emc.util.util).
-import(emc.connection).

-include("emc.hrl").

-define(CPOOL_TIMEOUT, 600000).

%% API
-export([start_link/0, connect/0, disconnect/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-record(state, { min,       %% Min reserved connections
		 max,       %% Max reserved connections
		 free = [], %% Free connections:  ConnPid
		 busy = [], %% Busy connections: {ConnPid, Owner(Pid)}
		 idle = []  %% Idle clients (waitng for connection): {Client(Pid), Ref(term)}
		}).

%%%-----------------------------------------------------------------------------
%%% API
%%%-----------------------------------------------------------------------------

%%
%%
%%
start_link() ->
    util:start_link_gen_server({local, ?MODULE}, ?MODULE, [], []).

%%
%%
%%
connect() ->
    case gen_server:call(?MODULE, {connect}, ?CPOOL_TIMEOUT) of
	{ok, ConnPid} ->
	    {ok, ConnPid};
	{wait, Ref} ->
	    receive
		{Ref, {ok, ConnPid}} -> {ok, ConnPid};
		{Ref, _Else}         ->  _Else
	    end;
	_Else ->
	    _Else
    end.

%%
%%
%%
disconnect(ConnPid) ->
    gen_server:call(?MODULE, {disconnect, ConnPid}, ?CPOOL_TIMEOUT).

%%%-----------------------------------------------------------------------------
%%% gen_server callbacks
%%%-----------------------------------------------------------------------------

%%
%%
%%
init(_) ->
    ?INFO(emc, io_lib:format("Initializing connection pool @ ~p", [node()])),
    process_flag(trap_exit, true),
    State0 = #state{ min = config:get('emc.cpool.min'),
		     max = config:get('emc.cpool.max') },
    init_pool(State0).

%%
%%
%%
handle_call({connect}, {Pid, _}, State) ->
    case get_pool_connection(State, Pid) of
	{ok, NewState, ConnPid} ->
	    {reply, {ok, ConnPid}, NewState};
	{wait, NewState, Ref} ->
	    {reply, {wait, Ref}, NewState};
	_Else ->
	    {reply, _Else, State}
    end;

handle_call({disconnect, ConnPid}, _, State) ->
    {ok, NewState} = free_pool_connection(State, ConnPid, false),
    {reply, ok, NewState};

handle_call(_, _, State) ->
    {reply, unknown_request, State}.

%%
%%
%%
handle_cast(_, State) ->
    {noreply, State}.

%%
%%
%%
handle_info({'EXIT', From, _Reason}, State) ->
    %% Check idle processes
    NewCPoolIdle = [ IdleTerm || {IdlePid, _}=IdleTerm <- State#state.idle,
				 IdlePid /= From ],
    NewState0 = State#state{ idle = NewCPoolIdle },
    %% Check busy processes
    OrphanConnPids = [ ConnPid || {ConnPid, Owner} <- NewState0#state.busy,
				  Owner == From ],
    NewState1 = lists:foldl(fun(ConnPid, StateAcc) ->
				    {ok, NewStateAcc} = free_pool_connection(StateAcc, ConnPid, false),
				    NewStateAcc
			    end, NewState0, OrphanConnPids),
    %% Check free dead connections
    NewCPoolFree = [ ConnPid || ConnPid <- NewState1#state.free,
				ConnPid /= From ],
    NewState2 = NewState1#state{ free  = NewCPoolFree },
    %% Finally check if is a dead busy connecttion
    {ok, NewState} = free_pool_connection(NewState2, From, true),
    %% Update state
    {noreply, NewState};

handle_info(_, State) ->
    {noreply, State}.

%%
%%
%%
terminate(Reason, State) ->
    [ catch connection:close(ConnPid) || ConnPid <- State#state.free ],
    [ catch connection:close(ConnPid) || {ConnPid, _} <- State#state.busy ],
    [ IdlePid ! {Ref, {error, cpool_shutdown}} || {IdlePid, Ref} <- State#state.idle ],
    ?INFO(emc, io_lib:format("Shutting down connection pool @ ~p: ", [Reason])),
    ok.

%%
%%
%%
code_change(_, State, _) ->
    {ok, State}.

%%%-----------------------------------------------------------------------------
%%% Connection pool management
%%%-----------------------------------------------------------------------------

%%
%%
%%
new_connection() ->
    connection:open(config:get('emc.cpool.hosts'), config:get('emc.cpool.behavior')).

%%
%%
%%
init_pool(State) ->
    ConnPids = [ case new_connection() of
		     {ok, ConnPid} -> ConnPid;
		     _Else         -> exit(_Else)
		 end
		 || _ <- lists:seq(1, State#state.min) ],
    NewState = State#state { free = ConnPids },
    {ok, NewState}.

%%
%%
%%
get_pool_connection(State, Pid) ->
    true = link(Pid),
    case State#state.free of
	%% There are available free connections
	[ ConnPid | RestConnPids ] ->
	    NewState = State#state{ free = RestConnPids,
				    busy = [{ConnPid, Pid} | State#state.busy]},
	    {ok, NewState, ConnPid};
	%% No available free connections...
	[] ->
	    case (length(State#state.free) + length(State#state.busy)) < State#state.max of
		%% ... but we can create new ones
		true ->
		    case new_connection() of
			{ok, ConnPid} ->
			    NewState = State#state{ busy = [{ConnPid, Pid} | State#state.busy] },
			    {ok, NewState, ConnPid};
			_Else ->
			    {error, unable_to_open_new_connection}
		    end;
		%% ... and but we can not create new ones => wait
		false ->
		    Ref = now(),
		    NewState = State#state{ idle = State#state.idle ++ [{Pid, Ref}] },
		    {wait, NewState, Ref}
	    end    
    end.

%%
%%
%%
free_pool_connection(State, ConnPid, Dead) ->
    case lists:keysearch(ConnPid, 1, State#state.busy) of
	{value, {ConnPid, Owner}} ->
	    NewState0 = State#state{ busy = lists:keydelete(ConnPid, 1, State#state.busy) },
	    %% Unlink from owner
	    case lists:keymember(Owner, 2, NewState0#state.busy) or lists:member(Owner, NewState0#state.idle) of
		true  -> true;
		false -> catch unlink(Owner)
			 end,
	    %% And...
	    case NewState0#state.idle of
		%% There are no clients waiting for a connection
		[] ->
		    case (length(NewState0#state.free) + length(NewState0#state.busy)) < NewState0#state.min of
			%% Return the connection to the free pool
			true ->
			    case Dead of
				%% The connection is ok
				false ->
				    NewState = NewState0#state{ free  = [ConnPid | NewState0#state.free] },
				    {ok, NewState};
				%% The connection has been tagged as invalid => try to recover it
				true ->
				    case new_connection() of
					%% Fresh connection
					{ok, NewConnPid} ->
					    NewState = NewState0#state{ free  = [NewConnPid | State#state.free] },
					    {ok, NewState};
					%% Failed to recover the connection => ignore; it'll be recovered in the future (get_pool_connection/2)
					_ ->
					    {ok, NewState0}
				    end
			    end;
			%% Minimum number of opened connections exceeded => close it!
			false ->
			    catch connection:close(ConnPid),
			    {ok, NewState0}
		    end;
		%% There is/are no clients waiting for a connection
		[ {IdlePid, Ref} | RestIdle ] ->
		    NewState1 = NewState0#state{ idle = RestIdle },
		    case Dead of
			%% The connection is ok => assing it to the older waiting process
			false ->
			    IdlePid ! {Ref, {ok, ConnPid}},
			    NewState = NewState1#state{ busy = [{ConnPid, IdlePid} | NewState1#state.busy] },
			    {ok, NewState};
			%% The connection has been tagged as invalid => try to recover it
			true ->
			    case new_connection() of
				%% Fresh connection
				{ok, NewConnPid} ->
				    IdlePid ! {Ref, {ok, NewConnPid}},
				    NewState = NewState1#state{ busy = [{NewConnPid, IdlePid} | NewState1#state.busy] },
				    {ok, NewState};
				%% Failed to recover the connection => ignore; it'll be recovered in the future (get_pool_connection/2)
				_ ->
				    IdlePid ! {Ref, {error, unable_to_restore_connection}},
				    {ok, NewState1}
			    end
		    end
	    end;
	_ ->
	    {ok, State}
    end.
